////////////////////////////////////////////////////////////////////////////////////////
//  IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING THIS SOFTWARE
//
//  By downloading, copying, installing or using the software you agree to this license.
//  If you do not agree to this license, do not download, install, copy or use the software.
//
// Copyright (C) 2013, AerVision Technologies Pty Ltd, all rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
//   * Redistribution's of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//
//   * Redistribution's in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//   * The name of AerVision Technologies, may not be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// This software is provided by the copyright holders and contributors to demonstrate 
// www.AerCloud.net Framework as a Service API (FaaS), "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the copyright holder or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
////////////////////////////////////////////////////////////////////////////////////////

package com.aercloud.client.util;

import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.imageio.ImageIO;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.WebServiceClient;

import org.apache.log4j.Logger;

import com.aercloud.client.api.DetectedObject;
import com.aercloud.client.api.IISMWebAPI;
import com.aercloud.client.api.IISMWebAPIDeleteImageFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPIDeleteImagesFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPIDetectFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPIGetImageNamesFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPIGetImagePngFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPIGetNamesFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPIQueryFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPISaveImageFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.IISMWebAPISearchByImageFaultExceptionFaultFaultMessage;
import com.aercloud.client.api.ISMWebAPI;
import com.aercloud.client.api.SearchResult;
import com.aercloud.client.gui.FilenameFilterImage;

public class AerCloudClient {
    
    public static final String ENDPOINT_ADDRESS = "https://www.aercloud.net/webapi";
    
    private static Logger log = Logger.getLogger(AerCloudClient.class);
    
    private static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    
    private IISMWebAPI ismWebApi;
    private FilenameFilterImage filenameFilterImage;
    
    private ExecutorService execPool;

    public AerCloudClient() {
        WebServiceClient ismWebApiWscAnnotation = 
            ISMWebAPI.class.getAnnotation(WebServiceClient.class);
        try {
            ismWebApi = 
                new ISMWebAPI(
                    new URL(ENDPOINT_ADDRESS + "?wsdl"), 
                    new QName(ismWebApiWscAnnotation.targetNamespace(), ismWebApiWscAnnotation.name()))
                        .getBasicHttpBindingIISMWebAPI();
        } catch (MalformedURLException e) {
            // actually unreachable since the URL is composed locally
            log.error("Failed to create service instance", e);
        }
        ((BindingProvider)ismWebApi).getRequestContext()
            .put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, ENDPOINT_ADDRESS);
//            .put(BindingProvider.SESSION_MAINTAIN_PROPERTY,true)
    }

    public ExecutorService getExecPool() {
        if(execPool == null) {
            execPool = Executors.newFixedThreadPool(10);
        }
        return execPool;
    }
    
    public boolean initConnection(String accessKey) {
        log.info("Connecting to the remote server ...");
        if(accessKey != null && accessKey.length() > 0) try {
            log.info("Sending a test query using provided access key. Text is 'HELLO'...");
            log.info("Received reply '" + ismWebApi.query(accessKey, "HELLO"));
            log.info("Excellent! everything works fine.");
            return true;
        } catch (IISMWebAPIQueryFaultExceptionFaultFaultMessage e) {
            log.error("Server Error> Received for provided access key: " + e.getMessage());
        } else {
            log.warn("Access key is required");
        }
        return false;
    }
    
    public BufferedImage saveImage(String accessKey, String fileName, BufferedImage image) {
        if(fileName != null && fileName.length() > 0 && getFilenameFilterImage().accept(null, fileName)) {
            String[] fileNameParts = fileName.split("\\.");
            BufferedImage faceImage = null;
            if(fileNameParts.length > 1) {
                String imageName = fileName.substring(0, fileName.lastIndexOf('.')).trim();
                String personName = fileNameParts[0].trim();
                try {
                    List<String> imageNames = 
                        getImageNames(accessKey, personName);
                    if(imageNames != null && imageNames.contains(imageName)) {
                        log.info("[" + imageName + "] " + "Already saved");
                        faceImage = 
                            getImage(accessKey, personName, imageName);
                    } else {
                        ByteArrayOutputStream baosPng = new ByteArrayOutputStream();
                        ImageIO.write(image, "png", baosPng);
                        List<DetectedObject> detectedObjects = 
                            ismWebApi.detect(accessKey, "face", "png", baosPng.toByteArray()).getDetectedObject();
                        if(detectedObjects.size() == 1) {
                            log.info("[" + imageName + "] " + "One face detected");
                        } else 
                        if(detectedObjects.size() > 1) {
                            log.info("[" + imageName + "] " + "There is more than one face present in this image, the biggest face is saved");
                        } else {
                            log.info("[" + imageName + "] " + "There is no face present in this image");
                            return null;
                        }
                        DetectedObject biggestFace = null;
                        for(DetectedObject detectedObj : detectedObjects) {
                            if(biggestFace == null || biggestFace.getWidth() < detectedObj.getWidth()) {
                                biggestFace = detectedObj;
                            }
                        }
                        faceImage = // The outer region of the face is used, because for a human viewer, more facial attributes are visible
                            cropFace(image, biggestFace);
                        baosPng.reset();
                        ImageIO.write(faceImage, "png", baosPng);
                        log.info("AerCloud.net call> [" + personName + " - " + imageName + "] Saving face");
                        if(ismWebApi.saveImage(accessKey, "face", "png", baosPng.toByteArray(), personName, imageName, true)) {
                            log.info("[" + personName + " - " + imageName + "] " + "Save successful");
                        } else {
                            log.info("[" + personName + " - " + imageName + "] + " + "Save failed");
                        }
                        String accessKeyHash = 
                            getHash(accessKey);
                        ImageCache.deleteImage(accessKeyHash, personName, imageName);
                        NameCache.addImageName(personName, imageName);
                    }
                } catch (IISMWebAPIDetectFaultExceptionFaultFaultMessage e) {
                    log.error("AerCloud.net ERROR> [" + imageName + "] " + e.getMessage());
                } catch (IISMWebAPISaveImageFaultExceptionFaultFaultMessage e) {
                    log.error("AerCloud.net ERROR> [" + imageName + "] " + e.getMessage());
                } catch (IOException e) {
                    log.error("ImageIO error: " + e.getMessage());
                }
            }
           return faceImage;
        }
        return null;
    }
    
    public List<String> getNames(String accessKey) {
        log.info("AerCloud.net call> Retrieving list of names from server...");
        try {
            return ismWebApi.getNames(accessKey).getString();
        } catch (IISMWebAPIGetNamesFaultExceptionFaultFaultMessage e) {
            log.error(e.getMessage());
        }
        return new ArrayList<String>();
    }
    
    public List<String> getImageNames(String accessKey, String personName) {
        List<String> imageNameList = 
            NameCache.getImageNames(personName);
        if(imageNameList == null) try {
            log.info("AerCloud.net call> Retrieving list of saved images from server...");
            imageNameList = 
                ismWebApi.getImageNames(accessKey, personName).getString();
            NameCache.addImageNames(personName, imageNameList);
        } catch (IISMWebAPIGetImageNamesFaultExceptionFaultFaultMessage e) {
            log.error("AerCloud.net ERROR> " + e.getMessage());
        }
        return (imageNameList != null)?imageNameList:new ArrayList<String>();
    }
    
    public BufferedImage getImage(String accessKey, String personName, String imageName) {
        String accessKeyHash = 
            getHash(accessKey);
        BufferedImage image = 
            ImageCache.getImage(accessKeyHash, personName, imageName);
        if(image == null) {
            log.info("AerCloud.net call> [" + personName + " - " + imageName + "] Retrieving face image...");
            try {
                image = 
                    ImageIO.read(new ByteArrayInputStream(ismWebApi.getImagePng(accessKey, personName, imageName)));
                ImageCache.putImage(accessKeyHash, personName, imageName, image);
            } catch (IISMWebAPIGetImagePngFaultExceptionFaultFaultMessage e) {
                log.error("AerCloud.net ERROR> [" + imageName + "] " + e.getMessage());
            } catch (IOException e) {
                log.error("ERROR> [" + imageName + "] " + e.getMessage());
            }
        }
        return image;
    }
    
    public Boolean deleteImage(String accessKey, String personName, String imageName) {
        log.info("AerCloud.net call> Delete image " + "[" + imageName + "]");
        Boolean deleteImageSuccessfull = null;
        try {
            deleteImageSuccessfull = ismWebApi.deleteImage(accessKey, personName, imageName);
            if(deleteImageSuccessfull) {
                ImageCache.deleteImage(getHash(accessKey), personName, imageName);
            }
        } catch (IISMWebAPIDeleteImageFaultExceptionFaultFaultMessage e) {
            log.error("AerCloud.net ERROR> [" + imageName + "] " + e.getMessage());
        }
        return deleteImageSuccessfull;
    }
    
    public Boolean deleteName(String accessKey, String personName) {
        log.info("AerCloud.net call> Delete name " + "[" + personName + "]");
        Boolean deleteNameSuccessfull = null;
        try {
            deleteNameSuccessfull = ismWebApi.deleteImages(accessKey, personName);
            if(deleteNameSuccessfull) {
                ImageCache.deleteAllImages(getHash(accessKey), personName);
            }
        } catch (IISMWebAPIDeleteImagesFaultExceptionFaultFaultMessage e) {
            log.error("AerCloud.net ERROR> [" + personName + "] " + e.getMessage());
        }
        return deleteNameSuccessfull;
    }
    
    public List<SearchResult> searchFace(String accessKey, String imageName, BufferedImage image) {
        log.info("AerCloud.net call> Search face " + "[" + imageName + "]");
        ByteArrayOutputStream baosPng = new ByteArrayOutputStream();
        try {
            ImageIO.write(image, "png", baosPng);
            return ismWebApi.searchByImage(accessKey, "face", "png", baosPng.toByteArray(), 10000, true).getSearchResult();
        } catch (IISMWebAPISearchByImageFaultExceptionFaultFaultMessage e) {
            log.error("AerCloud.net ERROR> [" + imageName + "] " + e.getMessage());
        } catch (IOException e) {
            log.error("ERROR> [" + imageName + "] " + e.getMessage());
        } finally {
            log.info("[" + imageName + "] Search completed");
        }
        return new ArrayList<SearchResult>();
    }
    
    public List<BufferedImage> detectFaces(String accessKey, String imageName, BufferedImage image) {
        log.info("AerCloud.net call> Detecting faces " + "[" + imageName + "]...");
        ByteArrayOutputStream baosPng = new ByteArrayOutputStream();
        List<BufferedImage> faces = new ArrayList<BufferedImage>();
        try {
            ImageIO.write(image, "png", baosPng);
            List<DetectedObject> detectedOjects =
                ismWebApi.detect(accessKey, "face", "png", baosPng.toByteArray()).getDetectedObject();
            if(detectedOjects.size() > 0) {
                if(detectedOjects.size() > 1) {
                    log.info("[" + imageName + "] " + detectedOjects.size() + " faces detected");
                } else {
                    log.info("[" + imageName + "] " + "One face detected");
                }
                for(DetectedObject faceRectangle : detectedOjects) {
                    faces.add(
                        cropFace(image, faceRectangle));
                }
            }
        } catch (IISMWebAPIDetectFaultExceptionFaultFaultMessage e) {
            log.error("AerCloud.net ERROR> [" + imageName + "] " + e.getMessage());
        } catch (IOException e) {
            log.error("ERROR> [" + imageName + "] " + e.getMessage());
        }
        return faces;
    }

    public FilenameFilterImage getFilenameFilterImage() {
        if(filenameFilterImage == null) {
            filenameFilterImage = new FilenameFilterImage();
        }
        return filenameFilterImage;
    }
    
    private static String toHexString(byte[] bytes) {
        char[] hexChars = new char[bytes.length << 1];
        int v;
        for ( int j = 0; j < bytes.length; j++ ) {
            v = bytes[j] & 0xFF;
            hexChars[j << 1] = hexArray[v >>> 4];
            hexChars[(j << 1) + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    } 
    
    private String getHash(String accessKey) {
        try {
            return toHexString(MessageDigest.getInstance("SHA-256").digest(accessKey.getBytes()));
        } catch (NoSuchAlgorithmException e) {
            log.error("Error obtaining an accessKey hash", e);
        }
        return null;
    }
    
    private BufferedImage cropFace(BufferedImage image, DetectedObject faceRectangle) {
        
        Rectangle rect = new Rectangle(
            faceRectangle.getX() - (int)(faceRectangle.getWidth() / 4d), 
            faceRectangle.getY() - (int)(faceRectangle.getHeight() * 0.4d), 
            (int)(faceRectangle.getWidth() * 1.5d), 
            (int)(faceRectangle.getHeight() * 1.5d));
        
        if (rect.x < 0)
        {
            rect.width += rect.x;
            rect.x = 0;
        }
        if (rect.y < 0)
        {
            rect.height += rect.y;
            rect.y = 0;
        }
        if ((rect.x + rect.width) > image.getWidth()) {
            rect.width += image.getWidth() - (rect.x + rect.width);
        }
        if ((rect.y + rect.height) > image.getHeight()) {
            rect.height += image.getHeight() - (rect.y + rect.height);
        }
        rect.width = Math.min(rect.width, rect.height);
        rect.height = rect.width;

        int d = Math.min(256, rect.width);
        
        return image.getSubimage(rect.x, rect.y, d, d);
    }
}
